/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * SPDX-License-Identifier: EPL-1.0
 ********************************************************************************/
package org.eclipse.ceylon.test.eclipse.plugin.util;

import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getProjectDeclaredSourceModules;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS_ERROR;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS_FAILED;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS_SKIPPED;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS_RUNNING;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TESTS_SUCCESS;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST_ERROR;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST_FAILED;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST_SKIPPED;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST_RUNNING;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.TEST_SUCCESS;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestImageRegistry.getImage;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestPlugin.CEYLON_TEST_MODULE_NAME;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestPlugin.LAUNCH_CONFIG_TYPE;
import static org.eclipse.ceylon.test.eclipse.plugin.CeylonTestPlugin.LAUNCH_CONFIG_TYPE_JS;
import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;

import org.eclipse.ceylon.common.Backend;
import org.eclipse.ceylon.ide.eclipse.core.builder.CeylonNature;
import org.eclipse.ceylon.model.typechecker.model.Annotation;
import org.eclipse.ceylon.model.typechecker.model.Class;
import org.eclipse.ceylon.model.typechecker.model.ClassOrInterface;
import org.eclipse.ceylon.model.typechecker.model.Declaration;
import org.eclipse.ceylon.model.typechecker.model.Function;
import org.eclipse.ceylon.model.typechecker.model.Module;
import org.eclipse.ceylon.model.typechecker.model.ModuleImport;
import org.eclipse.ceylon.model.typechecker.model.Package;
import org.eclipse.ceylon.model.typechecker.model.Type;
import org.eclipse.ceylon.model.typechecker.model.TypeDeclaration;
import org.eclipse.ceylon.model.typechecker.model.Value;
import org.eclipse.ceylon.test.eclipse.plugin.CeylonTestPlugin;
import org.eclipse.ceylon.test.eclipse.plugin.model.TestElement;

public class CeylonTestUtil {
    
    private static final String CEYLON_FILE_EXTENSION = "ceylon";
    
    private static final NumberFormat ELAPSED_TIME_FORMAT;
    static {
        ELAPSED_TIME_FORMAT = NumberFormat.getNumberInstance();
        ELAPSED_TIME_FORMAT.setGroupingUsed(true);
        ELAPSED_TIME_FORMAT.setMinimumFractionDigits(3);
        ELAPSED_TIME_FORMAT.setMaximumFractionDigits(3);
        ELAPSED_TIME_FORMAT.setMinimumIntegerDigits(1);
    }
    
    public static Display getDisplay() {
        Display display= Display.getCurrent();
        if (display == null) {
            display= Display.getDefault();
        }
        return display;
    }
    
    public static IWorkspaceRoot getWorkspaceRoot() {
        return ResourcesPlugin.getWorkspace().getRoot();
    }

    public static IWorkbenchWindow getActiveWorkbenchWindow() {
        return CeylonTestPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow();
    }

    public static IWorkbenchPage getActivePage() {
        IWorkbenchWindow activeWorkbenchWindow = getActiveWorkbenchWindow();
        if (activeWorkbenchWindow != null) {
            return activeWorkbenchWindow.getActivePage();
        }
        return null;
    }

    public static Shell getShell() {
        IWorkbenchWindow activeWorkbenchWindow = getActiveWorkbenchWindow();
        if (activeWorkbenchWindow != null) {
            return activeWorkbenchWindow.getShell();
        }
        return null;
    }
    
    public static List<IProject> getProjects() {
        List<IProject> ceylonProjects = new ArrayList<IProject>();
        IProject[] projects = getWorkspaceRoot().getProjects();
        if (projects != null) {
            for (IProject project : projects) {
                if (isCeylonProject(project)) {
                    ceylonProjects.add(project);
                }
            }
        }
        return ceylonProjects;
    }

    public static IProject getProject(String projectName) {
        List<IProject> projects = getProjects();
        for (IProject project : projects) {
            if (project.getName().equals(projectName)) {
                return project;
            }
        }
        return null;
    }
    
    public static IProject getProject(ILaunch launch) throws CoreException {
    	ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
    	String projectName = launchConfiguration.getAttribute(ATTR_PROJECT_NAME, (String) null);
    	return getProject(projectName);
    }

    public static Module getModule(IProject project, String moduleName) {
        for (Module module : getProjectDeclaredSourceModules(project)) {
            if (module.getNameAsString().equals(moduleName)) {
                return module;
            }
        }
        return null;
    }

    public static Package getPackage(IProject project, String pkgName) {
        for (Module module : getProjectDeclaredSourceModules(project)) {
            Package pkg = module.getDirectPackage(pkgName);
            if (pkg != null) {
                return pkg;
            }
        }
        return null;
    }
    
    public static Object getPackageOrDeclaration(IProject project, String qualifiedName) {
    	Object result = null;
    	
		String pkgName = null;
		int pkgSepIndex = qualifiedName.indexOf("::");
		if (pkgSepIndex == -1) {
			pkgName = qualifiedName;
		} else {
			pkgName = qualifiedName.substring(0, pkgSepIndex);
		}
		
		Package pkg = getPackage(project, pkgName);
		if (pkg != null && pkgSepIndex != -1) {
			Declaration d;
			int memberSepIndex = qualifiedName.indexOf(".", pkgSepIndex);
			if (memberSepIndex != -1) {
				String baseName = qualifiedName.substring(pkgSepIndex + 2, memberSepIndex);
				String methodName = qualifiedName.substring(memberSepIndex + 1);
				d = pkg.getMember(baseName, null, false);
				d = extractAnonymousClassIfRequired(d);
				if (d != null) {
					Declaration m = d.getMember(methodName, null, false);
					if( m instanceof Function && d instanceof Class ) {
					    result = new MethodWithContainer((Class)d, (Function)m);
					}
				}
			} else {
				String baseName = qualifiedName.substring(pkgSepIndex + 2);
				d = pkg.getMember(baseName, null, false);
				result = extractAnonymousClassIfRequired(d);
			}
		} else {
			result = pkg;
		}
    	
    	return result;
    }
    
    public static List<MethodWithContainer> getAllMethods(ClassOrInterface c) {
        List<MethodWithContainer> members = new ArrayList<MethodWithContainer>();
        getAllMethods(c, c, members);
        return members;
    }

    private static void getAllMethods(ClassOrInterface c, TypeDeclaration t, List<MethodWithContainer> members) {
        for (Declaration d : t.getMembers()) {
            if (d instanceof Function) {
                Function m = (Function) d;
                boolean contains = false;
                for (MethodWithContainer member : members) {
                    String name = member.getMethod().getName();
                    if (name!=null && name.equals(m.getName())) {
                        contains = true;
                        break;
                    }
                }
                if (!contains) {
                    members.add(new MethodWithContainer(c, m));
                }
            }
        }
        Type et = t.getExtendedType();
        if (et != null) {
            getAllMethods(c, et.getDeclaration(), members);
        }
        for (Type st : t.getSatisfiedTypes()) {
            getAllMethods(c, st.getDeclaration(), members);
        }
    }
    
    public static Declaration extractAnonymousClassIfRequired(Declaration d) {
        if (d instanceof Value) {
            Value value = (Value) d;
            TypeDeclaration typeDeclaration = value.getTypeDeclaration();
            if (typeDeclaration instanceof Class && typeDeclaration.isAnonymous()) {
                return typeDeclaration;
            }
        }
        return d;
    }
    
    public static boolean isCeylonProject(IProject project) {
        return project.isOpen() && CeylonNature.isEnabled(project);
    }
    
    public static boolean isCeylonFile(IFile file) {
        return isCeylonProject(file.getProject()) && CEYLON_FILE_EXTENSION.equals(file.getFileExtension());
    }

    public static boolean isTestable(Object element) {
        if (element instanceof IProject || element instanceof Module || element instanceof Package) {
            return true;
        }
        else if( element instanceof Class ) {
            return isTestableClass((Class) element);
        }
        else if( element instanceof Function ) {
            return isTestableMethod((Function) element, null);
        }
        else if( element instanceof MethodWithContainer ) {
            MethodWithContainer m = (MethodWithContainer) element;
            return isTestableMethod(m.getMethod(), m.getContainer());
        }
        return false;
    }

    private static boolean isTestableClass(Class clazz) {
        if (clazz.isToplevel() && !clazz.isAbstract()) {
            List<MethodWithContainer> methods = getAllMethods(clazz);
            for (MethodWithContainer method : methods) {
                if (containsTestAnnotation(method.getMethod())) {
                    return true;
                }
            }
        }
        return false;
    }

    private static boolean isTestableMethod(Function method, TypeDeclaration container) {
        boolean isTestableMethod = false;
        if (method.isToplevel() || (container instanceof Class && isTestableClass((Class) container))) {
            if (!method.isFormal() && containsTestAnnotation(method)) {
                isTestableMethod = true;
            }
        }
        return isTestableMethod;
    }
    
    private static boolean containsTestAnnotation(Function method) {
        List<Annotation> annotations = method.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.getName().equals("test")) {
                return true;
            }
        }
        return false;
    }
    
    public static Image getTestStateImage(TestElement testElement) {
        Image image = null;
        if(testElement != null) {
            if( testElement.getChildren() == null || testElement.getChildren().size() == 0 ) {
                switch(testElement.getState()) {
                case RUNNING: image = getImage(TEST_RUNNING); break;
                case SUCCESS: image = getImage(TEST_SUCCESS); break;
                case FAILURE: image = getImage(TEST_FAILED); break;
                case ERROR: image = getImage(TEST_ERROR); break;
                case SKIPPED_OR_ABORTED: image = getImage(TEST_SKIPPED); break;
                default: image = getImage(TEST); break;
                }
            } else {
                switch(testElement.getState()) {
                case RUNNING: image = getImage(TESTS_RUNNING); break;
                case SUCCESS: image = getImage(TESTS_SUCCESS); break;
                case FAILURE: image = getImage(TESTS_FAILED); break;
                case ERROR: image = getImage(TESTS_ERROR); break;
                case SKIPPED_OR_ABORTED: image = getImage(TESTS_SKIPPED); break;
                default: image = getImage(TESTS); break;
                }
            }
        }
        return image;
    }
    
    public static String getElapsedTimeInSeconds(long milis) {
        double seconds = milis / 1000.0;
        return ELAPSED_TIME_FORMAT.format(seconds);
    }
    
    public static boolean containsCeylonTestImport(Module module) {
        if( module != null ) {
            for (ModuleImport moduleImport : module.getImports()) {
                if (moduleImport.getModule().getNameAsString().equals(CEYLON_TEST_MODULE_NAME)) {
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean checkNativeBackend(Module module, String launchConfigType) {
        if (module.isNative()) {
            if (Objects.equals(launchConfigType, LAUNCH_CONFIG_TYPE) && !module.getNativeBackends().supports(Backend.Java.asSet())) {
                return false;
            }
            if (Objects.equals(launchConfigType, LAUNCH_CONFIG_TYPE_JS) && !module.getNativeBackends().supports(Backend.JavaScript.asSet())) {
                return false;
            }
        }
        return true;
    }

}